/* -*- c -*- */
/* Copyright:    (C) 2005 - 2017 by Dipl.-Ing. Stefan Heesch
 * callsign: HB9TWS
 * email:    radio@heesch.net
 */

/* Morse drill addition: March 2019
 * Chris Baird VK2CJB <cjb@brushtail.apana.org.au>
 *
 * Usage:
 *
 * Straight key support only.
 * Holding down the key during power-up/reset starts the trainer
 * in Stefan's original 'decode only' mode.
 * Otherwise, it shows a pseudo-random word from a "100 most common
 * words in CW" list I found. Complete it correctly to get another
 * one. (And you just have to manage the letters, not the whole word
 * perfectly.) Repeat. Repeat. Repeat. Repeat...
 *
 * To change the testing pool of words, bang in "TEST", and then a
 * number --
 * 1 = "100 common CW words"
 * 2 = Single letters and digits
 * 3 = 8-letter uncommon-ish words
 * 4 = single letters and digits, with the morse code displayed
 * 5 = punctuation, with the morse (what I'm currently studying..)
 * 6 = a poem.. (that is some English text)
 * 7 = pseudo-callsigns
 * 0 = decode only mode
 *
 * Changing the sidetone is "nnFRQ", where nn is the frequency/100,
 * "06FRQ" for example makes it 600 Hz.
 *
 * "nnWPM" enters 'copy mode', where the trainer beeps out words from
 * the current test pool at the given nn-digits speed, and you would
 * be doing something like try to write them down. The included poem
 * is useful here. Hold down the key to exit.
 */

#include <LiquidCrystal.h>
#include <EEPROM.h>
#include "morse.h"

LiquidCrystal lcd(9, 8, 5, 4, 3, 2);

int buzzer   = A5;
int morsekey = A4;
int buzzerfreq = 700;

#define KEY_DELAY 15		/* debounce limit, milliseconds */
#define LINE_LENGTH 16

cw  m_Signal;
int m_KeyState;
int m_LastKeyState;

char    buffer[LINE_LENGTH+1];
char    m_Line[LINE_LENGTH+1];
uint8_t m_Position;
char    last = ' ';

/* ---------------------------------------------------------------------- */

extern void morse_init (int speed);
extern char morse_decode (cw input);
extern char* morse_failed (void);
extern int morse_speed (void);
extern char morse_check (cw input);
extern int morse_timeout (void);

void newtestword (void);
void flushline (void);
void updatetestline (void);
void show (char s);
void copystring (char* s);
void copymode (void);
void decode ();
int readmorsekey (void);
void setup ();
void loop ();

/* ---------------------------------------------------------------------- */
// Note the trailing spaces in the strings so that they concatenate

const char commontext[] PROGMEM =
  "QRL QRM QRN QRQ QRS QRZ QTH QSB QSY "
  "TU TNX NAME RST CQ AGN ANT DX ES HI OM PSE PWR WX 73 "
  "ABOUT ALL AM AN AND ANY ARE AS AT BE BEEN BEFORE BUT BY CAN COME "
  "COULD DO DONT EVERY FIRST FOR FROM GO GREAT HAD HAS HAVE HE HER HIM "
  "HIS HOW IF IN INTO IS IT ITS JUST KNOW LIKE LITTLE MADE MAN MAY ME "
  "MEAN MEN MORE MUST MY NO NOT NOW OF OH ON ONE ONLY OR OTHER OUR OUT "
  "OVER PEOPLE REALLY RIGHT SAID SAY SHALL SHE SHOULD SO SOME SUCH THAN "
  "THAT THATS THE THEIR THEM THEN THERE THESE THEY THINK THIS TIME TO "
  "TWO UH UP UPON US VERY WAS WE WELL WERE WHAT WHEN WHERE WHICH WHO "
  "WILL WITH WORK WOULD YEAH YOU YOUR "
  "123 1971 1980 2011 1955 1945 812837 73 "
  "BENSBEST WIRE SISTER";

const char longtext[] PROGMEM =
  "AFFECTED AFLUKING AHANKARA AMMONIUM BEROIDAE BLAHLAUT "
  "CLOWNERY COERCION DEJECTED DICLINIC DIDYMATE ECHOWISE "
  "ERGATOID EXCRESCE FASCISTA FJERDING FUNMAKER GEOBLAST "
  "HELICOID HERMITRY INERTIAL LENINITE LUTETIAN LYREBIRD "
  "MURRELET OROMETRY OUTVALUE OVEREDIT PANORAMA PEYERIAN "
  "REEDBUCK SKELPING SUBTOTAL SUITHOLD TOTAQUIN UNCOATED "
  "UNHERDED BETATRON BREWSTER CADUCARY DIPHENOL EMPOCKET "
  "FEEDBACK FRONDOUS HERODIAN IRONCLAD IZCATECO KAMMALAN "
  "NODICORN ORYZOMYS OXYUROUS PORINGLY RACHITIS ROSEMARY "
  "SHAVIANA SILICIUM STICKLER STRUMPET SYLPHISH THICKETY "
  "TURBINED TYRANNUS UNDERLAY UNHEIRED UNHINTED UPSTROKE";

const char singletext[] PROGMEM =
  "A B C D E F G H I J K L M N O P Q R S T U V W X Y Z "
  "0 1 2 3 4 5 6 7 8 9";

const char singletextcheat[] PROGMEM =
  "A_.- B_-... C_-.-. D_-.. E_. F_..-. G_--. H_.... I_.. J_.--- K_-.- "
  "L_.-.. M_-- N_-. O_--- P_.--. Q_--.- R_.-. S_... T_- U_..- V_...- "
  "W_.-- X_-..- Y_-.-- Z_--.. 0_----- 1_.---- 2_..--- 3_...-- 4_....- "
  "5_..... 6_-.... 7_--... 8_---.. 9_----.";

const char symboltext[] PROGMEM =
  ",_--..-- ._.-.-.- ?_..--.. /_-..-. =_-...- -_-....- :_---... "
  ";_-.-.-. (_-.--. )_-.--.- '_.----. \"_.-..-. @_.--.-.";

const char lawson[] PROGMEM =
  "ABOVE THE ASHES STRAIGHT AND TALL THROUGH FERNS WITH MOISTURE DRIPPING "
  "I CLIMB BENEATH THE SANDSTONE WALL MY FEET ON MOSSES SLIPPING "
  "LIKE RAMPARTS ROUND THE VALLEYS EDGE THE TINTED CLIFFS ARE STANDING "
  "WITH MANY A BROKEN WALL AND LEDGE AND MANY A ROCKY LANDING "
  "AND ROUND ABOUT THEIR RUGGED FEET DEEP FERNY DELLS ARE HIDDEN "
  "IN SHADOWED DEPTHS WHENCE DUST AND HEAT ARE BANISHED AND FORBIDDEN "
  "THE STREAM THAT CROONING TO ITSELF COMES DOWN A TIRELESS ROVER "
  "FLOWS CALMLY TO THE ROCKY SHELF AND THERE LEAPS BRAVELY OVER "
  "NOW POURING DOWN NOW LOST IN SPRAY WHEN MOUNTAIN BREEZES SALLY "
  "THE WATER STRIKES THE ROCK MIDWAY AND LEAPS INTO THE VALLEY "
  "NOW IN THE WEST THE COLOURS CHANGE THE BLUE WITH CRIMSON BLENDING "
  "BEHIND THE FAR DIVIDING RANGE THE SUN IS FAST DESCEND "
  "AND MELLOWED DAY COMES OVER THE PLACE AND SOFTENS RAGGED EDGES "
  "THE RISING MOONS GREAT PLACID FACE LOOKS GRAVELY OVER THE LEDGES "
  "= BLUE MOUNTAINS BY HENRY LAWSON 1888.";

const char callsigntemplate[] PROGMEM =
  "@n@@ @n@@@ n@n@@@ n@@n@@ @@n@@ @@n@@@ @@nn@@ "
  "VKn@@@ VKn@@ VKnF@@@ ZLn@@ ZLn@@@";

const char* const texts[] PROGMEM =
  {
   0, commontext, singletext, longtext, singletextcheat, symboltext,
   lawson, callsigntemplate
  };

#define numtexts (sizeof(texts)/sizeof(char*))

char testword[12];
char testpnt;
unsigned char testmode = 1;

/* ---------------------------------------------------------------------- */
/* pick a ~random~ word in a space-delimited string */

unsigned int lp = 0;

void newtestword (void)
{
  unsigned int f = pgm_read_word(&texts[testmode]);
  int i = 1;

  if (testmode != 6)		/* make the poem go sequentially */
    i += millis() & 63;

  for (; i; i--)
    {
      char c;
      int j = 0;
      while ((c = pgm_read_byte(f+lp)) > ' ')
	{
	  testword[j++] = c;
	  lp = (c ? lp+1 : 0);
	}
      testword[j] = 0;
      lp = (c == ' ' ? lp+1 : 0);
    }

  if (testword[1] == '_') testword[1] = 32;

  if (testmode == 7)
    {
      randomSeed (millis());
      for (i = 0; testword[i]; i++)
	{
	  if (testword[i] == '@')
	    do testword[i] = 65 + (random() % 26);
	    while (testword[i] == 'Q'); /* most countries avoid it */
	  if (testword[i] == 'n')
	    testword[i] = 48 + (random() % 10);
	}
    }

  testpnt = 0;
}

/* ---------------------------------------------------------------------- */

void flushline (void)
{
  for (int i = 0; i < LINE_LENGTH-1; i++)
    m_Line[i] = ' ';
  m_Position = LINE_LENGTH-1;
}

/* ---------------------------------------------------------------------- */

void updatetestline (void)
{
  if (testmode)
    {
      int p;

      lcd.setCursor(0, 0);
      for (p = 0; testword[p]; p++)
	lcd.write((char)testword[p]);
      while (p++ < (LINE_LENGTH-6))
	lcd.write(' ');

      lcd.cursor();
      lcd.setCursor(testpnt, 0);
    }
  else
      lcd.noCursor();
}

/* ---------------------------------------------------------------------- */

void show (char s)
{
  m_Line[m_Position] = s;

  if (++m_Position > LINE_LENGTH-1)
    {
      for (int i = 0; i < LINE_LENGTH-1; i++)
	m_Line[i] = m_Line[i + 1];

      m_Line[LINE_LENGTH-1] = ' ';
      m_Position = LINE_LENGTH-1;
    }

  lcd.setCursor(0, 1);
  lcd.print(m_Line);

  updatetestline();
}

/* ---------------------------------------------------------------------- */

int copywpm = 10;

/* Idea from 'Efficient Storage Of Morse Code Character Codes',
 * Lawrence Krakauer, page 36, Issue 14, BYTE 1976-10 */
const unsigned char morse_table[] PROGMEM =
  {
   'A', 0b110,    'B', 0b10001,  'C', 0b10101,  'D', 0b1001,
   'E', 0b10,     'F', 0b10100,  'G', 0b1011,   'H', 0b10000,
   'I', 0b100,    'J', 0b11110,  'K', 0b1101,   'L', 0b10010,
   'M', 0b111,    'N', 0b101,    'O', 0b1111,   'P', 0b10110,
   'Q', 0b11011,  'R', 0b1010,   'S', 0b1000,   'T', 0b11,
   'U', 0b1100,   'V', 0b11000,  'W', 0b1110,   'X', 0b11001,
   'Y', 0b11101,  'Z', 0b10011,  '0', 0b111111, '1', 0b111110,
   '2', 0b111100, '3', 0b111000, '4', 0b110000, '5', 0b100000,
   '6', 0b100001, '7', 0b100011, '8', 0b100111, '9', 0b101111,
   ',', 0b1110011,'.', 0b1101010,'?', 0b1001100,'/', 0b101001,
   '=', 0b110001, '-', 0b1100001,':', 0b1000111,';', 0b1010101,
   '(', 0b101101, ')', 0b1101101,'\'',0b1011110,'"', 0b1010010,
   '@', 0b1010110,
   0, 0
  };

void copystring (char* s)
{
  int dit = 2000 / copywpm;

  while (*s)
    {
      int i;

      if (*s == '_') break;

      for (i = 0; pgm_read_byte(&morse_table[i]); i += 2)
	if (pgm_read_byte(&morse_table[i]) == *s) break;

      if (pgm_read_byte(&morse_table[i]))
	{
	  unsigned char b = pgm_read_byte(&morse_table[i+1]);
	  while (b != 1)
	    {
	      tone (buzzer, buzzerfreq);
	      if (b & 1) delay (3*dit); /* do dash (3 dits) */
	      else       delay (dit); /* do dit (1 dit) */
	      noTone (buzzer);

	      delay(dit);	/* do inter-symbol delay (1 dit) */
	      b >>= 1;
	    }
	}
      else
	delay (5*dit);		/* assume it's a space (7-2 dits) */

      delay (2*dit);		/* do intra-letter space (3-1 dits) */
      s++;
    }
}

void copymode (void)
{
  do
    {
      lcd.clear();
      flushline();
      lcd.noCursor();
      lcd.print(F("COPY.KEY TO EXIT"));
      newtestword();
      copystring(testword);
      for (int i = 0; testword[i]; i++)
	{
	  if (readmorsekey()) break;
	  delay(750);
	}
    }
  while (readmorsekey()== 0);
}

/* ---------------------------------------------------------------------- */

void decode()
{
  static char cmdbuf[5];
  cw input;

  input.level = m_Signal.level;
  input.duration = m_Signal.duration;

  char s = morse_decode(input);

  if (s > 0)
    {
      last = s;
      switch (s)
	{
	case 'a': show('a'); show('r'); break;
	case 'k': show('k'); show('a'); break;
	case 's': show('s'); show('k'); break;
	case DECODING_ERROR: /*show('_');*/ break;
	default: show(s); break;
	}

      if (s != ' ')
	{
	  lcd.setCursor(0, 0);
	  if (s == DECODING_ERROR)
	    {
	      for (char *p = morse_failed(); *p; p++)
		show(*p);
	    }
	  else
	    {
	      sprintf(buffer, "          %2d wpm", morse_speed() / 5);
	      lcd.print(buffer);
	      updatetestline();
	    }
	}

      /* command trigger */
      for (int i = 4; i; i--) cmdbuf[i] = cmdbuf[i-1];
      cmdbuf[0] = s;

      if ((cmdbuf[4] == 'T') && (cmdbuf[3] == 'E') && (cmdbuf[2] == 'S') &&
	  (cmdbuf[1] == 'T'))
	{
	  if ((s > 47) && (s < (48+numtexts)))
	    {
	      delay (50);
	      testmode = s - 48;
	      updateeeprom();
	      lp = 0;
	      newtestword();
	      tone (buzzer, 800); delay (100);
	      tone (buzzer, 1600); delay (100);
	      noTone(buzzer);
	      lcd.clear();
	      if (testmode == 0) lcd.print (F("DECODE MODE"));
	      flushline();
	    }
	}

      /* copy mode */
      if ((cmdbuf[2] == 'W') && (cmdbuf[1] == 'P') && (cmdbuf[0] == 'M'))
	{
	  copywpm = (cmdbuf[4]*10) + cmdbuf[3] - 480 - 48;
	  if ((copywpm > 4) && (copywpm < 41))
	    copymode();
	}

      /* change sidetone */
      if ((cmdbuf[2] == 'F') && (cmdbuf[1] == 'R') && (cmdbuf[0] == 'Q'))
	{
	  int i = (cmdbuf[4]*10) + cmdbuf[3] - 480 - 48;
	  if ((i > 1) && (i < 30))
	    {
	      delay (50);
	      tone (buzzer, 1000); delay (50);
	      tone (buzzer, 900); delay (50);
	      noTone(buzzer);
	      buzzerfreq = i * 100;
	      updateeeprom();
	    }
	}

      /* jym joke */
      if ((cmdbuf[4] == 'R') && (cmdbuf[3] == 'U') && (cmdbuf[2] == 'O') &&
	  (cmdbuf[1] == 'K') && (cmdbuf[0] == '?'))
	{
	  copywpm = morse_speed() / 5;
	  copystring ("HUG ME");
	}

      /* completed */
      if ((testmode) && (s == testword[testpnt]))
	{
	  testpnt++;
	  if (testword[testpnt] < 33)
	    {
	      delay (50);
	      tone (buzzer, 1200); delay (50);
	      tone (buzzer, 1600); delay (50);
	      noTone(buzzer);
	      newtestword();
	      flushline();
	    }
	}
    }

  /* Update speed and check for new word */
  s = morse_check(input);
  if (s == ' ' && last != ' ' && m_Position > 0)
    {
      last = s;
      show(s);
    }
}

/* ---------------------------------------------------------------------- */

int readmorsekey (void)
{
  return digitalRead(morsekey) > 0 ? 0 : 1;
}

/* ---------------------------------------------------------------------- */

void updateeeprom (void)
{
  EEPROM.write(0,testmode);
  EEPROM.write(1, buzzerfreq >> 8);
  EEPROM.write(2, buzzerfreq & 255);
}

void setup()
{
  testmode = EEPROM.read(0);
  buzzerfreq = (EEPROM.read(1) << 8) + EEPROM.read(2);
  if (testmode > numtexts) testmode = 1;
  if ((buzzerfreq < 100) || (buzzerfreq > 3000)) buzzerfreq = 700;

  for (int i = 0; i<LINE_LENGTH-1; i++)
    {
      buffer[i] = 0;
      m_Line[i] = ' ';
    }
  buffer[LINE_LENGTH - 1] = 0;
  m_Line[LINE_LENGTH - 1] = 0;
  flushline();

  digitalWrite(morsekey, HIGH);
  pinMode(morsekey, INPUT_PULLUP);

  if (readmorsekey())
    testmode = 0; /* key down -> start with original decoder */

  lcd.begin(LINE_LENGTH, 2);
  lcd.setCursor(0, testmode ? 0 : 1);
  lcd.print(F(" Morse Trainer"));
  lcd.setCursor(0, testmode ? 1 : 0);
  lcd.print(F("HB9TWS & VK2CJB"));

  tone(buzzer, 324); delay(300);
  tone(buzzer, 648); delay(300);
  tone(buzzer, 1292); delay(300);
  noTone(buzzer); delay(1500);

  morse_init(60);

  while (readmorsekey()) continue; /* key up before continuing */
  m_KeyState = 0;
  m_LastKeyState = 0;
  m_Signal.level = 0;
  m_Signal.duration = 0;

  updateeeprom();
  lcd.clear();
  newtestword();
  updatetestline();
  if (testmode == 0) lcd.print (F("DECODE MODE"));
}

/* ---------------------------------------------------------------------- */

void loop()
{
  static unsigned long m_Start = 0;
  static unsigned long m_DebounceTime;

  unsigned long now = millis();

  if (m_Start == 0)
    {
      m_Start = now;
      m_DebounceTime = now;
      return;
    }

  m_KeyState = readmorsekey();

  if (m_KeyState != m_LastKeyState)
    m_DebounceTime = millis();

  if (now - m_DebounceTime > KEY_DELAY)
    {
      if (m_Signal.level != m_KeyState)
	{
	  m_Signal.duration = now - m_Start;
	  decode();
	  m_Signal.level = m_KeyState;
	  m_Start = now;
	}

      if (now - m_Start > morse_timeout() && m_Signal.level == 0)
	{
	  m_Signal.duration = now - m_Start;
	  decode();
	  m_Start = now;
	}
    }

  /* side tone */

  m_LastKeyState = m_KeyState;

  if (m_KeyState == 1)
    tone(buzzer, buzzerfreq);
  else
    noTone(buzzer);

}

/* ---------------------------------------------------------------------- */
